﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using StyleMVVM.Conventions.DataConverter;
using StyleMVVM.Conventions.Service;
using StyleMVVM.Data.Activation;
using StyleMVVM.DependencyInjection;
using StyleMVVM.Logging;
using StyleMVVM.View;
using StyleMVVM.ViewModel;
#if WINDOWS_PHONE
using Windows.Foundation.Collections;
#endif
#if NETFX_CORE
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Media;

#else
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;

#endif

namespace StyleMVVM.Conventions.ViewModel
{
	public sealed class ConventionViewModelBinder : IViewModelBinder, IDependencyInjectionContainerAware
	{
		private static string supplemental = typeof(ConventionViewModelBinder).FullName;
		private IConventionsService conventionsService;
		private IActivationService activationService;
		private Lazy<IViewModelResolutionService> viewModelResolutionServiceLazy;

		public static void RegisterExport(IDependencyInjectionContainer container)
		{
			container.Register(typeof(ConventionViewModelBinder))
			         .As(typeof(IViewModelBinder))
			         .ImportMethod("ImportServices")
			         .AndShared();
		}

		public void BindViewModelToView(FrameworkElement frameworkElement, object viewModel)
		{
#if DOT_NET
			if (frameworkElement.IsLoaded)
			{
				InternalBindViewModelToView(frameworkElement, viewModel);
			}
			else
			{
				// we need to bind properties after the control 
				frameworkElement.Loaded += FrameworkElementOnLoaded;
			}
#else
			if (frameworkElement.Parent != null)
			{
				InternalBindViewModelToView(frameworkElement, viewModel);
			}
			else
			{
				// we need to bind properties after the control 
				frameworkElement.Loaded += FrameworkElementOnLoaded;
			}
#endif
		}

		private void FrameworkElementOnLoaded(object sender, RoutedEventArgs routedEventArgs)
		{
			FrameworkElement frameworkElement = sender as FrameworkElement;

			if (frameworkElement != null)
			{
				InternalBindViewModelToView(frameworkElement, frameworkElement.DataContext);

				frameworkElement.Loaded -= FrameworkElementOnLoaded;
			}
		}

		private void InternalBindViewModelToView(FrameworkElement frameworkElement, object viewModel)
		{
			List<UserControl> userControls = new List<UserControl>();
			List<FrameworkElement> namedControls = FindNamedControls(frameworkElement, userControls);

			namedControls.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase));

#if !DOT_NET
			IObservableMap<string, object> map = viewModel as IObservableMap<string, object>;

			if (map != null)
			{
				BindObservableMap(map, namedControls);
			}
#endif

			BindProperties(viewModel, namedControls);
			BindHandlerMethods(viewModel, namedControls);

			foreach (UserControl userControl in userControls)
			{
				if (!HasDataContext(userControl))
				{
					foreach (string viewModelNameConvention in conventionsService.ViewModelNameConventions(userControl.GetType()))
					{
						if (ViewModelResolutionService.ResolveViewModel(userControl, viewModelNameConvention))
						{
							break;
						}
					}
				}
			}
		}

		public void UnbindViewModelFromView(FrameworkElement frameworkElement, object viewModel)
		{
		}

		public void ImportServices(IConventionsService conventionsService, IActivationService activationService)
		{
			this.conventionsService = conventionsService;
			this.activationService = activationService;
		}

		private IViewModelResolutionService ViewModelResolutionService
		{
			get { return Container.LocateByType(typeof(IViewModelResolutionService)) as IViewModelResolutionService; }
		}

		private bool HasDataContext(FrameworkElement frameworkElement)
		{
			object newValue = frameworkElement.ReadLocalValue(FrameworkElement.DataContextProperty);

			if (newValue == DependencyProperty.UnsetValue)
			{
				return false;
			}

			return true;
		}

		private List<FrameworkElement> FindNamedControls(DependencyObject rootControl, List<UserControl> userControls)
		{
			Queue<DependencyObject> controlQueue = new Queue<DependencyObject>();
			List<FrameworkElement> returnValue = new List<FrameworkElement>();

			controlQueue.Enqueue(rootControl);

			while (controlQueue.Count > 0)
			{
				DependencyObject currentControl = controlQueue.Dequeue();
				FrameworkElement frameworkElement = currentControl as FrameworkElement;

				if (frameworkElement == null)
				{
					continue;
				}

				if (!string.IsNullOrEmpty(frameworkElement.Name))
				{
					returnValue.Add(frameworkElement);
				}

				if (frameworkElement is UserControl && !Equals(rootControl, frameworkElement))
				{
					userControls.Add(frameworkElement as UserControl);
					continue;
				}

				int childCount = VisualTreeHelper.GetChildrenCount(frameworkElement);

				if (childCount > 0)
				{
					for (int i = 0; i < childCount; i++)
					{
						DependencyObject childObject = VisualTreeHelper.GetChild(frameworkElement, i);

						controlQueue.Enqueue(childObject);
					}
				}
				else
				{
					ContentControl contentControl = frameworkElement as ContentControl;

					if (contentControl != null && contentControl.Content is DependencyObject)
					{
						controlQueue.Enqueue(contentControl.Content as DependencyObject);
					}
				}
			}

			return returnValue;
		}

#if !DOT_NET
		private void BindObservableMap(IObservableMap<string, object> viewModel, List<FrameworkElement> namedControls)
		{
		}
#endif

		private void BindProperties(object viewModel, List<FrameworkElement> namedControls)
		{
			IEnumerable<PropertyInfo> properties = GetProperties(viewModel);
			int numControls = namedControls.Count;

			foreach (PropertyInfo property in properties)
			{
				string propertyName = property.Name;
				char propertyFirstChar = Char.ToLower(propertyName[0]);
				int startPoint = 0;

				if (numControls > 8)
				{
					startPoint = CalculateStartPoint(namedControls, numControls, propertyFirstChar);
				}

				for (int i = startPoint; i < numControls; i++)
				{
					FrameworkElement namedControl = namedControls[i];
					string controlName = namedControl.Name;
					char nameFirstChar = Char.ToLower(controlName[0]);

					if (nameFirstChar < propertyFirstChar)
					{
						continue;
					}

					if (nameFirstChar > propertyFirstChar)
					{
						break;
					}

					if (propertyName.StartsWith(controlName, StringComparison.OrdinalIgnoreCase))
					{
						DependencyProperty dependencyProperty = null;

						if (controlName.Length == propertyName.Length)
						{
							dependencyProperty =
								conventionsService.GetPropertyConvention(namedControl.GetType(), property);
						}
						else
						{
							string dependencyPropertyName = property.Name.Substring(controlName.Length);

							dependencyProperty = GetDependencyProperty(namedControl.GetType(), dependencyPropertyName);
						}

						if (dependencyProperty != null && !HasBinding(namedControl, dependencyProperty))
						{
							Binding newBinding = new Binding();

							newBinding.Path = new PropertyPath(property.Name);

							if (property.CanWrite)
							{
								newBinding.Mode = BindingMode.TwoWay;
							}
							else
							{
								newBinding.Mode = BindingMode.OneWay;
							}

							foreach (Attribute customAttribute in property.GetCustomAttributes())
							{
								IDataConverterAttribute converterAttribute = customAttribute as IDataConverterAttribute;

								if (converterAttribute != null && converterAttribute.Type != null)
								{
									IValueConverter converter =
										activationService.CreateByType(
											Container.CreateInjectionContext(), converterAttribute.Type) as IValueConverter;

									if (converter != null)
									{
										IDataConverterControlAware viewAware = converter as IDataConverterControlAware;
										IDataConverterViewModelAware viewModelAware = converter as IDataConverterViewModelAware;

										if (viewAware != null)
										{
											viewAware.Control = namedControl;
										}

										if (viewModelAware != null)
										{
											viewModelAware.ViewModel = viewModel;
										}

										newBinding.Converter = converter;
									}
									else
									{
										Logger.Error("Could not create type: " + converterAttribute.Type.FullName, supplemental);
									}
									break;
								}
							}

							BindingOperations.SetBinding(namedControl, dependencyProperty, newBinding);

#if DOT_NET
							StyleMVVM.View.ValidationAdorner.SetIsEnabled(namedControl, true);
#else
							StyleMVVM.View.Validation.SetProperty(namedControl, property.Name);
#endif
						}
					}
				}
			}
		}

		private static int CalculateStartPoint(List<FrameworkElement> namedControls, int numControls, char propertyFirstChar)
		{
			int startPoint = 0;
			int middleIndex = (numControls - 1) / 2;
			char middleChar = Char.ToLower(namedControls[middleIndex].Name[0]);

			if (middleChar < propertyFirstChar)
			{
				startPoint = middleIndex;
			}

			int quarterMark = startPoint + (numControls / 4);
			char quarterChar = Char.ToLower(namedControls[quarterMark].Name[0]);

			if (quarterChar < propertyFirstChar)
			{
				startPoint = quarterMark;
			}
			return startPoint;
		}

		private bool HasBinding(DependencyObject element, DependencyProperty property)
		{
#if DOT_NET
			return BindingOperations.GetBindingBase(element, property) != null;
#else
			object localValue = element.ReadLocalValue(property);

			if (localValue == DependencyProperty.UnsetValue || localValue == null)
			{
				return false;
			}

			return localValue.GetType().FullName == "System.__ComObject";
#endif
		}

		private DependencyProperty GetDependencyProperty(Type controlType, string propertyName)
		{
#if NETFX_CORE
			PropertyInfo propertyInfo = controlType.GetTypeInfo().GetDeclaredProperty(propertyName + "Property");

			if (propertyInfo != null)
			{
				return propertyInfo.GetValue(null) as DependencyProperty;
			}
#else
			FieldInfo fieldInfo = controlType.GetField(propertyName + "Property");

			if (fieldInfo != null)
			{
				return fieldInfo.GetValue(null) as DependencyProperty;
			}
#endif

			if (controlType.GetTypeInfo().BaseType != null)
			{
				return GetDependencyProperty(controlType.GetTypeInfo().BaseType, propertyName);
			}

			return null;
		}

		private void BindHandlerMethods(object viewModel,
		                                List<FrameworkElement> namedControls)
		{
			IEnumerable<MethodInfo> handlerMethods = GetHandlerMethods(viewModel);
			int numControls = namedControls.Count;

			foreach (MethodInfo handlerMethod in handlerMethods)
			{
				string methodName = handlerMethod.Name;
				char methodFirstChar = Char.ToLower(methodName[0]);
				string conventionName = handlerMethod.Name.Substring(0, methodName.Length - "Handler".Length);
				int startPoint = 0;

				if (numControls > 8)
				{
					startPoint = CalculateStartPoint(namedControls, numControls, methodFirstChar);
				}

				for (int i = startPoint; i < numControls; i++)
				{
					FrameworkElement namedControl = namedControls[i];
					string controlName = namedControl.Name;
					char controlFirstChar = Char.ToLower(controlName[0]);

					if (controlFirstChar < methodFirstChar)
					{
						continue;
					}

					if (controlFirstChar > methodFirstChar)
					{
						break;
					}

					if (conventionName.StartsWith(controlName))
					{
						string eventString = conventionName.Substring(controlName.Length);

						EventInfo eventInfo = GetEventInfo(namedControl.GetType(), eventString);

						if (eventInfo != null)
						{
							StringBuilder parameterString = new StringBuilder();

							foreach (ParameterInfo parameterInfo in handlerMethod.GetParameters())
							{
								parameterString.AppendFormat("${0},", parameterInfo.Name);
							}

							string attachString =
								string.Format("{0} => {1}({2})", eventInfo.Name, handlerMethod.Name, parameterString.ToString().TrimEnd(','));

							EventHandlerInstance handlerInstance = new EventHandlerInstance { Attach = attachString };

							EventHandlerList list = EventHandlers.GetList(namedControl);

							if (list == null)
							{
								list = new EventHandlerList { handlerInstance };

								EventHandlers.SetList(namedControl, list);
							}
							else
							{
								list.Add(handlerInstance);

								handlerInstance.Connect(namedControl);
							}
						}
						else
						{
							Logger.Debug(string.Format("Could not find event {0} on control {1} for bind method {2}",
							                           eventString,
							                           controlName,
							                           handlerMethod.Name));
						}
					}
				}
			}
		}

		private EventInfo GetEventInfo(Type type, string eventName)
		{
			EventInfo eventInfo = type.GetRuntimeEvent(eventName);

			if (eventInfo == null)
			{
				Type baseType = type.GetTypeInfo().BaseType;

				if (baseType != null)
				{
					return GetEventInfo(type.GetTypeInfo().BaseType, eventName);
				}
			}

			return eventInfo;
		}

		/// <summary>
		/// Returns all public properties
		/// </summary>
		/// <param name="viewModel"></param>
		/// <returns></returns>
		private IEnumerable<PropertyInfo> GetProperties(object viewModel)
		{
#if DOT_NET
			ICustomTypeProvider provider = viewModel as ICustomTypeProvider;

			if (provider != null)
			{
				return provider.GetCustomType().GetProperties();
			}
#endif

			return viewModel.GetType().GetTypeInfo().DeclaredProperties;
		}

		/// <summary>
		/// Returns all methods that end with Handler
		/// </summary>
		/// <param name="viewModel"></param>
		/// <returns></returns>
		private IEnumerable<MethodInfo> GetHandlerMethods(object viewModel)
		{
			foreach (MethodInfo declaredMethod in viewModel.GetType().GetTypeInfo().DeclaredMethods)
			{
				if (declaredMethod.Name.ToLower().EndsWith("handler"))
				{
					yield return declaredMethod;
				}
			}
		}

		public IDependencyInjectionContainer Container { get; set; }
	}
}